hono-remix-adapterを試して、Cloudflare Pagesにデプロイしてみた
はじめに
こんにちは、コンサルティング部の神野です。
最近Hono開発者のWada Yusuke氏がXでhono-remix-adapter
を発表していました!
スクリーンショットにある設定を入れるだけでReactをベースとしたフルスタックフレームワークRemixとHonoを簡単に組み合わせて使用できそうなので気になって試してみました!
フロントエンドにHonoのフレームワークを活用してバックエンドのAPIも実行できるのは嬉しいですよね!!Honoの強みであるフロントエンド・バックエンド連携を容易にするRPC機能も試していきたいと思います。
またデプロイ先にCloudflare Pagesをサポートしているので、デプロイも実施していきます。
作成するアプリケーションのイメージ
下記スクリーンショットみたいな簡単なTodoアプリケーションを作成します。
Todoの取得や更新はバックエンドのサーバーを通して行うこととします。
最終的にはCloudflare Pagesにデプロイするところまでをゴールとします。
作成したアプリケーションのレポジトリ
今回作成したアプリケーションのレポジトリは下記に格納しております。
全体を参照したいなど必要に応じてご参照いただけますと幸いです。
準備
今回はCloudflare Pagesを使ってデプロイを行うため、もしデプロイまで進めたい方は事前にアカウントを作成する必要があります!
クレジットカードの情報なども不要で、メールアドレスだけで登録可能なのでお手軽です!
実装
事前準備
今回はNode.jsおよびパッケージマネージャーとしてnpmを使って進めていくので下記を事前にインストールしています。
- Node.js・・・v20.16.0
- npm・・・10.8.1
まずは早速Remixを作成していきます。下記コマンドを実行してテンプレートを作成できます。
npm create cloudflare@latest -- my-remix-app --framework=remix
作成後は使用するライブラリをインストールします。
cd my-remix-app
npm install hono hono-remix-adapter @hono/zod-validator
shadcn/uiセットアップ
今回はUIライブラリのshadcn/uiを使用して画面を作成します。
公式ドキュメントのインストール手順に従ってセットアップを行います。
セットアップが完了したら今回使用するコンポーネントをインストールしていきます。
npx shadcn@latest add badge card button checkbox
honoセットアップ
vite.config.js
にビルド時のHono用のプラグインを設定します。サーバー処理側のエントリポイントを設定します。
import {
vitePlugin as remix,
cloudflareDevProxyVitePlugin as remixCloudflareDevProxy,
} from "@remix-run/dev";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
+ import adapter from "@hono/vite-dev-server/cloudflare"
+ import serverAdapter from "hono-remix-adapter/vite"
export default defineConfig({
plugins: [
remixCloudflareDevProxy(),
remix({
future: {
v3_fetcherPersist: true,
v3_relativeSplatPath: true,
v3_throwAbortReason: true,
},
}),
+ serverAdapter({
+ adapter,
+ entry:"./server/index.ts"
+ }),
tsconfigPaths(),
],
});
環境変数の設定
.env
ファイルを作成し下記値を設定します。これはクライアントからバックエンド側にリクエストを送信する際のURLを設定しています。Cloudflare Pagesにデプロイする際は別途設定します。
VITE_API_URL=http://localhost:5173/
バックエンド実装
次にHonoでバックエンド側の処理を実装していきます。server
フォルダにindex.ts
ファイルを作成して、バックエンド側の処理を記載します。今回はダミーのTodo情報を返却及び更新する簡易的な処理を実装します。
GET
でダミーのTodo情報を一覧で取得し、PUT
でTodoが完了したかどうかのcompleted
を更新- CORSミドルウェアを適用
- Zodを使用したバリデーションスキーマを定義し、リクエストデータの型安全性を確保
import { Hono } from "hono";
import { cors } from "hono/cors";
import { z } from "zod";
import { zValidator } from "@hono/zod-validator";
const app = new Hono();
// カスタムZodスキーマ for YYYY-MM-DD形式の日付
const dateSchema = z.string().refine(
(val) => {
return /^\d{4}-\d{2}-\d{2}$/.test(val) && !isNaN(Date.parse(val));
},
{
message: "Invalid date format. Use YYYY-MM-DD",
}
);
// Todoのスキーマ
const TodoSchema = z.object({
title: z.string().min(1).max(100),
completed: z.boolean(),
dueDate: dateSchema.optional(),
});
// すべてのルートにCORS設定を適用
app.use(
"*",
cors({
origin: "*",
allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowHeaders: ["Content-Type", "Authorization"],
exposeHeaders: ["Content-Length"],
maxAge: 600,
})
);
// ダミーのTodo情報
const dummyTodos = [
{
id: 1,
text: "買い物に行く",
completed: false,
dueDate: "2023-06-15",
category: "日常",
},
{
id: 2,
text: "レポートを書く",
completed: true,
dueDate: "2023-06-10",
category: "仕事",
},
{
id: 3,
text: "運動する",
completed: false,
dueDate: "2023-06-16",
category: "健康",
},
{
id: 4,
text: "本を読む",
completed: false,
dueDate: "2023-06-20",
category: "趣味",
},
{
id: 5,
text: "友達と電話する",
completed: true,
dueDate: "2023-06-12",
category: "社交",
},
];
const route = app
.get("/api/todos", (c) => {
return c.json(dummyTodos);
})
.put("/api/todos/:id", zValidator("json", TodoSchema), async (c) => {
const id = c.req.param("id");
const validatedData = c.req.valid("json");
return c.json({ id: id, completed: !validatedData.completed });
});
export default app;
// クライアント側で型情報を参照するためexport
export type AppType = typeof route;
画面実装
Todo一覧画面を作成していきます。特徴としては以下で特にバックエンドに型安全を担保しつつリクエストが送信できるRPC機能が魅力的です。
- RPC機能を活用してフロントエンドからバックエンド側にリクエストを送信
- バックエンドからimportした型定義を元にクライアントを作成しリクエストを送信することが可能
- 一覧取得はサーバサイドレンダリング時に取得
- チェックボックス押下時はクライアントからバックエンドからリクエストを送信し、更新を実施
import type { MetaFunction } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { Button } from "~/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { Checkbox } from "~/components/ui/checkbox";
import { Badge } from "~/components/ui/badge"; // shadcn/uiのBadgeコンポーネントを追加
import { PlusCircle } from "lucide-react"; // アイコンをインポート
import { AppType } from "server/index";
import { hc } from "hono/client";
import { useState } from "react";
export const meta: MetaFunction = () => {
return [{ title: "Todo My App" }];
};
// HonoのRPC機能でClient作成
const client = hc<AppType>(import.meta.env.VITE_API_URL);
// 初回データフェッチ
export const loader = async () => {
const res = await client.api.todos.$get();
return res.json();
};
// カテゴリーに応じた色を定義
const categoryColors: { [key: string]: string } = {
日常: "bg-blue-100 text-blue-800",
仕事: "bg-purple-100 text-purple-800",
健康: "bg-green-100 text-green-800",
趣味: "bg-yellow-100 text-yellow-800",
社交: "bg-pink-100 text-pink-800",
};
// TodoのCard部分
const TodoItem = ({
todo,
}: {
todo: Awaited<ReturnType<typeof loader>>[number];
}) => {
const [isCompleted, setIsCompleted] = useState(todo.completed);
const badgeColor =
categoryColors[todo.category] || "bg-gray-100 text-gray-800";
const handleCheckboxChange = async (checked: boolean) => {
const result = await client.api.todos[":id"].$put({
json: {
title: todo.text,
completed: isCompleted,
},
param: {
id: todo.id.toString(),
},
});
const updatedTodo = await result.json();
setIsCompleted(updatedTodo.completed);
};
return (
<Card className="mb-4 hover:shadow-md transition-shadow duration-200">
<CardContent className="flex items-center p-4">
<Checkbox
id={`todo-${todo.id}`}
checked={isCompleted}
onCheckedChange={handleCheckboxChange}
className="mr-4"
/>
<div className="flex-grow">
<label
htmlFor={`todo-${todo.id}`}
className={`text-lg font-medium leading-none ${
isCompleted ? "line-through text-gray-400" : "text-gray-700"
}`}
>
{todo.text}
</label>
<div className="mt-2 flex items-center space-x-2">
<Badge className={`${badgeColor} font-semibold`}>
{todo.category}
</Badge>
<span className="text-sm text-gray-500">期日: {todo.dueDate}</span>
</div>
</div>
</CardContent>
</Card>
);
};
const Todo = () => {
const todos = useLoaderData<typeof loader>();
return (
<div className="min-h-screen bg-gray-50">
<header className="bg-white text-gray-800 p-4 shadow-sm">
<div className="container mx-auto">
<h1 className="text-2xl font-bold">My TODO App</h1>
</div>
</header>
<main className="container mx-auto py-8 px-4">
<div className="w-full max-w-4xl mx-auto">
<div className="flex flex-row items-center justify-between mb-6">
<h2 className="text-2xl font-bold text-gray-700">TODOリスト</h2>
<Button className="bg-black text-white hover:bg-gray-800 transition-colors duration-200 rounded-xl px-4 py-2">
<PlusCircle className="mr-2 h-4 w-4" />
新しいTODOを追加
</Button>
</div>
<div className="space-y-4">
{todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} />
))}
</div>
</div>
</main>
</div>
);
};
export default Todo;
補足:RPC機能について
ここでRPC機能を使ってフロント・バックエンドの連携を容易に実現しましたが、自動でコード補完や型安全が担保されるのは大変便利で開発体験は素晴らしいなと感じました!!
今後も積極的に使っていきたい機能です。
参考:Honoを紹介した際の登壇資料抜粋
動作確認
実装が一通り完了したので、サーバーを立ち上げて動作確認してみます。
サーバーはhttp://localhost:5173
で起動します。
npm run dev
サーバーが立ち上がったところで早速画面にアクセスしてみます!
Todo画面:http://localhost:5173/todos
問題なく画面が表示されていますね!
チェックボックスを押下しても問題なく反映されているので、API側サーバとの疎通は取得・更新ともに問題ないですね!
ちなみにAPI部分にリクエスト送信すると、問題なくレスポンスが返却されます。
これで実装は完了です!!RemixとHonoは序盤設定がいくつかあったものの思ったより簡単に統合できました!!バックエンド側でHonoを使ってAPIサーバの機能を持たせられるのは強いですね・・・!!
デプロイ
hono-remix-adapter
はデプロイ先にCloudflare Pagesをサポートしているのでデプロイしてみます!
デプロイに当たっていくつか準備および設定を行います。
事前準備
functions/[[path]].ts
ファイルを作成し、下記コードを実装します。 このファイルは、Cloudflare Pagesがリクエストを処理する際の入口点となります。
import handle from "hono-remix-adapter/cloudflare-pages";
import * as build from "../build/server";
import hono from "../server";
export const onRequest = handle(build, hono);
この設定で、Cloudflare Pagesは リクエストをHonoを通してRemixにルーティングし、適切なレスポンスを生成できるようになります。これによって、RemixアプリケーションとHonoのAPIを統合して、Cloudflare Pages上で動作させることが可能になります。
GithubへソースコードをPush
Cloudflare Pagesへデプロイする際にGithubのレポジトリと連携して自動でデプロイが可能なので、レポジトリを作成してPushしておきます。
デプロイ実施
Cloudflare Pagesにログインして、作業を進めていきます。Workers & Pagesタブの概要を押下し、Gitに接続ボタンを押下します。
その後、自分がRemixのアプリケーションをPushしたレポジトリを連携します。
設定項目 | 設定値 |
---|---|
Github アカウント | 使用するGithub アカウント |
レポジトリ | RemixをPushしたレポジトリ |
ビルドとデプロイのセットアップに伴って下記入力項目を設定します。
設定項目 | 設定値 |
---|---|
プロジェクト名 | 任意のプロジェクト名(私はそのままレポジトリ名にしました) |
プロダクション ブランチ | 任意のブランチ |
フレームワーク プリセット | Remix |
ビルド コマンド | npm run build(デフォルト) |
ビルド出力ディレクトリ | build/client |
環境変数 | VITE_API_URL = https://<プロジェクト名>.dev/ |
デプロイが完了するまでしばらく待って、下記画面が表示されて成功したら完了です!
デプロイされたアプリケーションのURLにアクセスすると問題なく表示されていますね!
これでデプロイも無事完了しました!簡単にデプロイできて素晴らしいですね!
おわりに
hono-remix-adapter
を使って簡単にRemixとHonoを統合できて素晴らしいですね。軽量なアプリケーションならこれで問題なく事足りそうな印象でした。
現時点でのサポートはCloudflare Pagesのみですが、今後もアップデートがなされてサポートが増えた場合は引き続き試していきたいと思います!
最後までご覧いただきありがとうございました!!